﻿using System.Linq;
using IGS.Core.Evaluators;
using IGS.Core.Integrators;
using IGS.Core.Logging;
using IGS.Core.Systems;
using Moq;
using NUnit.Framework;

namespace IGS.Core.Tests
{
    public static class SimulatorTests
    {
        [TestFixture]
        public class GivenTwoBodySystemWithInitialConditions
        {
            private static Mock<INBodySystem> SetupNBodySystem(IInteractable[] bodies)
            {
                var system = new Mock<INBodySystem>();
                system.Setup(s => s.GetEnumerator()).Returns(() => bodies.AsEnumerable().GetEnumerator());
                system.SetupGet(s => s.Length).Returns(bodies.Length);

                for (int i = 0; i < bodies.Length; i++)
                {
                    var index = i;
                    system.Setup(s => s[index]).Returns(bodies[index]);
                }

                return system;
            }

            [Test]
            public void WhenTheSystemIsInstantiatedThenBodiesShouldBeResetBeforeForcesAreEvaluated()
            {
                // Arrange
                var body1 = new Mock<IInteractable>();
                var body2 = new Mock<IInteractable>();
                var bodies = new[] { body1.Object, body2.Object };
                var system = SetupNBodySystem(bodies);
                var evaluator = new Mock<IForceEvaluator>();
                var integrator = new Mock<IIntegrator>();
                var logger = new Mock<ILogger>();

                // Act
                var simulator = new Simulator(system.Object, evaluator.Object, integrator.Object, logger.Object);

                // Assert
                Assert.IsNotNull(simulator, "Simulator");
                body1.Verify(b => b.Reset());
                body2.Verify(b => b.Reset());
            }

            [Test]
            public void WhenTheSystemIsInstanstiatedThenForcesShouldBeEvaluatedAfterBodiesAreReset()
            {
                // Arrange
                var body1 = new Mock<IInteractable>();
                var body2 = new Mock<IInteractable>();
                var bodies = new[] { body1.Object, body2.Object };
                var system = SetupNBodySystem(bodies);
                var evaluator = new Mock<IForceEvaluator>();
                var integrator = new Mock<IIntegrator>();
                var logger = new Mock<ILogger>();

                // Act
                var simulator = new Simulator(system.Object, evaluator.Object, integrator.Object, logger.Object);

                // Assert
                Assert.IsNotNull(simulator, "Simulator");
                evaluator.Verify(e => e.Evaluate(system.Object));
            }

            [Test]
            public void WhenTheSystemIsInstanstiatedThenBodiesShouldEndTheCurrentIntegrationStepAfterForcesAreEvaluated()
            {
                // Arrange
                var body1 = new Mock<IInteractable>();
                var body2 = new Mock<IInteractable>();
                var bodies = new[] { body1.Object, body2.Object };
                var system = SetupNBodySystem(bodies);
                var evaluator = new Mock<IForceEvaluator>();
                var integrator = new Mock<IIntegrator>();
                var logger = new Mock<ILogger>();

                // Act
                var simulator = new Simulator(system.Object, evaluator.Object, integrator.Object, logger.Object);

                // Assert
                Assert.IsNotNull(simulator, "Simulator");
                body1.Verify(b => b.EndStep());
                body2.Verify(b => b.EndStep());
            }

            [Test]
            public void WhenTheSystemIsEvolvedThenTheBodiesShouldBePredictedBeforeEvaluatingPairwiseGravity()
            {
                // Arrange
                var body1 = new Mock<IInteractable>();
                var body2 = new Mock<IInteractable>();
                var bodies = new[] { body1.Object, body2.Object };
                var system = SetupNBodySystem(bodies);
                var evaluator = new Mock<IForceEvaluator>();
                var integrator = new Mock<IIntegrator>();
                var logger = new Mock<ILogger>();
                var simulator = new Simulator(system.Object, evaluator.Object, integrator.Object, logger.Object);
                const double dt = 0.01;

                // Act
                simulator.Evolve(dt);

                // Assert
                integrator.Verify(i => i.Predict(system.Object, dt));
            }
            
            [Test]
            public void WhenTheSystemIsEvolvedThenBodyShouldBeResetAfterPredictionAndBeforeForceEvaluation()
            {
                // Arrange
                var body1 = new Mock<IInteractable>();
                body1.Setup(b => b.Reset());
                var body2 = new Mock<IInteractable>();
                body2.Setup(b => b.Reset());
                var bodies = new[] { body1.Object, body2.Object };
                var system = SetupNBodySystem(bodies);
                var evaluator = new Mock<IForceEvaluator>();
                var integrator = new Mock<IIntegrator>();
                var logger = new Mock<ILogger>();
                var simulator = new Simulator(system.Object, evaluator.Object, integrator.Object, logger.Object);
                const double dt = 0.01;

                // Act
                simulator.Evolve(dt);

                // Assert
                body1.Verify(b => b.Reset());
                body2.Verify(b => b.Reset());
            }

            [Test]
            public void WhenTheSystemIsEvolvedThenForcesShouldBeEvaluatedAfterBodiesAreResetAndBeforeCorrection()
            {
                // Arrange
                var body1 = new Mock<IInteractable>();
                var body2 = new Mock<IInteractable>();
                var bodies = new[] { body1.Object, body2.Object };
                var system = SetupNBodySystem(bodies);
                var evaluator = new Mock<IForceEvaluator>();
                var integrator = new Mock<IIntegrator>();
                var logger = new Mock<ILogger>();
                var simulator = new Simulator(system.Object, evaluator.Object, integrator.Object, logger.Object);
                const double dt = 0.01;

                // Act
                simulator.Evolve(dt);

                // Assert
                evaluator.Verify(e => e.Evaluate(system.Object));
            }

            [Test]
            public void WhenTheSystemIsEvolvedThenTheBodiesShouldBeCorrectedAfterEvaluatingPairwiseGravity()
            {
                // Arrange
                var body1 = new Mock<IInteractable>();
                var body2 = new Mock<IInteractable>();
                var bodies = new[] { body1.Object, body2.Object };
                var system = SetupNBodySystem(bodies);
                var evaluator = new Mock<IForceEvaluator>();
                var integrator = new Mock<IIntegrator>();
                var logger = new Mock<ILogger>();
                var simulator = new Simulator(system.Object, evaluator.Object, integrator.Object, logger.Object);
                const double dt = 0.01;

                // Act
                simulator.Evolve(dt);

                // Assert
                integrator.Verify(i => i.Correct(system.Object, dt));
            }

            [Test]
            public void WhenTheBodiesAreCorrectedThenTheBodiesShouldEndTheCurrentIntegrationStep()
            {
                // Arrange
                var body1 = new Mock<IInteractable>();
                body1.Setup(b => b.EndStep());
                var body2 = new Mock<IInteractable>();
                body2.Setup(b => b.EndStep());
                var bodies = new[] { body1.Object, body2.Object };
                var system = SetupNBodySystem(bodies);
                var evaluator = new Mock<IForceEvaluator>();
                var integrator = new Mock<IIntegrator>();
                var logger = new Mock<ILogger>();
                var simulator = new Simulator(system.Object, evaluator.Object, integrator.Object, logger.Object);
                const double dt = 0.01;

                // Act
                simulator.Evolve(dt);

                // Assert
                body1.Verify(b => b.EndStep());
                body2.Verify(b => b.EndStep());
            }

            [Test]
            public void WhenTheSystemIsEvolvedTheInfoLogShouldOutputTheStateOfTheBodies()
            {
                // Arrange
                var body1 = new Mock<IInteractable>();
                body1.Setup(b => b.ToString()).Returns("Body 1");
                var body2 = new Mock<IInteractable>();
                body2.Setup(b => b.ToString()).Returns("Body 1");
                var bodies = new[] { body1.Object, body2.Object };
                var system = SetupNBodySystem(bodies);
                var evaluator = new Mock<IForceEvaluator>();
                var integrator = new Mock<IIntegrator>();
                var logger = new Mock<ILogger>();
                var simulator = new Simulator(system.Object, evaluator.Object, integrator.Object, logger.Object);
                const double dt = 0.01;

                // Act
                simulator.Evolve(dt);

                // Assert
                logger.Verify(log => log.LogInfo(body1.Object.ToString()));
                logger.Verify(log => log.LogInfo(body2.Object.ToString()));
            }

            [Test]
            public void WhenTheSystemIsEvolvedTheInfoLogShouldOutputTheTotalKineticEnergyOfTheBodies()
            {
                // Arrange
                var body1 = new Mock<IInteractable>();
                var body2 = new Mock<IInteractable>();
                var bodies = new[] { body1.Object, body2.Object };
                var evaluator = new Mock<IForceEvaluator>();
                var integrator = new Mock<IIntegrator>();
                var logger = new Mock<ILogger>();
                var system = SetupNBodySystem(bodies);
                system.SetupGet(s => s.KineticEnergy).Returns(1.0);
                var simulator = new Simulator(system.Object, evaluator.Object, integrator.Object, logger.Object);
                const double dt = 0.01;

                // Act
                simulator.Evolve(dt);

                // Assert
                system.VerifyGet(s => s.KineticEnergy);
                logger.Verify(log => log.LogInfo("Kinetic Energy = {0:0.000}", +1.0));
            }

            [Test]
            public void WhenTheSystemIsEvolvedTheInfoLogShouldOutputTheTotalPotentialEnergyOfTheBodies()
            {
                // Arrange
                var body1 = new Mock<IInteractable>();
                var body2 = new Mock<IInteractable>();
                var bodies = new[] { body1.Object, body2.Object };
                var evaluator = new Mock<IForceEvaluator>();
                var integrator = new Mock<IIntegrator>();
                var logger = new Mock<ILogger>();
                var system = SetupNBodySystem(bodies);
                system.SetupGet(s => s.PotentialEnergy).Returns(-1.0);
                var simulator = new Simulator(system.Object, evaluator.Object, integrator.Object, logger.Object);
                const double dt = 0.01;

                // Act
                simulator.Evolve(dt);

                // Assert
                logger.Verify(log => log.LogInfo("Potential Energy = {0:0.000}", -1.0));
            }
        }
    }
}
